import { NextRequest, NextResponse } from 'next/server' import { createServerSupabaseClient } from '@/lib/supabase' export async function POST( request: NextRequest, { params }: { params: { id: string } } ) { try { const supabase = createServerSupabaseClient() const bookId = params.id const { currentFiles } = await request.json() // Get current user session const authHeader = request.headers.get('authorization') let user try { if (authHeader) { const { data: { user: authUser }, error: authError } = await supabase.auth.getUser(authHeader.replace('Bearer ', '')) if (!authError) user = authUser } else { const { data: { user: sessionUser }, error: sessionError } = await supabase.auth.getUser() if (!sessionError) user = sessionUser } } catch (e) { // Ignore auth errors } if (!user) { return NextResponse.json( { error: 'Authentication required' }, { status: 401 } ) } // Get GitHub integration const { data: profile } = await supabase .from('profiles') .select('github_integrations') .eq('id', user.id) .single() const integration = profile?.github_integrations?.[bookId] if (!integration) { return NextResponse.json( { error: 'GitHub integration not found' }, { status: 404 } ) } const owner = integration.github_username const repo = integration.repository_name const accessToken = integration.access_token try { // Get the diff between current files and the latest commit const diffResult = await generateGitStyleDiff( owner, repo, accessToken, currentFiles ) return NextResponse.json({ changes: diffResult.changes, summary: diffResult.summary, diffText: diffResult.diffText }) } catch (githubError) { console.error('❌ GitHub Diff API: Error generating diff:', githubError) return NextResponse.json( { error: 'Failed to generate diff', details: githubError instanceof Error ? githubError.message : String(githubError) }, { status: 500 } ) } } catch (error) { return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ) } } // Helper function to generate git-style diff async function generateGitStyleDiff( owner: string, repo: string, accessToken: string, currentFiles: Array<{ name: string; content: string; path: string }> ): Promise<{ changes: Array<{ path: string changeType: 'added' | 'modified' | 'deleted' linesAdded: number linesRemoved: number currentContent: string committedContent: string }> summary: { filesChanged: number insertions: number deletions: number } diffText: string }> { console.log('📊 GitHub Diff API: Generating git-style diff') // Get the latest commit SHA const latestCommitResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/commits/HEAD`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }) if (!latestCommitResponse.ok) { if (latestCommitResponse.status === 409) { // Repository is empty - all files are new const changes = currentFiles.map(file => ({ path: file.path, changeType: 'added' as const, linesAdded: file.content.split('\n').length, linesRemoved: 0, currentContent: file.content, committedContent: '' })) const summary = { filesChanged: changes.length, insertions: changes.reduce((sum, change) => sum + change.linesAdded, 0), deletions: 0 } return { changes, summary, diffText: generateDiffText(changes) } } throw new Error(`Failed to get latest commit: ${latestCommitResponse.status}`) } const latestCommit = await latestCommitResponse.json() // Get the tree for the latest commit const treeResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${latestCommit.sha}?recursive=1`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }) if (!treeResponse.ok) { throw new Error(`Failed to get repository tree: ${treeResponse.status}`) } const treeData = await treeResponse.json() const committedFiles: { [path: string]: string } = {} // Get content for all committed files const fileBlobs = treeData.tree.filter((item: any) => item.type === 'blob' && item.path !== 'README.md' && !item.path.startsWith('.git') ) console.log(`📊 GitHub Diff API: Processing ${fileBlobs.length} committed files`) // Fetch committed file contents in batches const batchSize = 10 for (let i = 0; i < fileBlobs.length; i += batchSize) { const batch = fileBlobs.slice(i, i + batchSize) await Promise.all(batch.map(async (blob: any) => { try { const blobResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/blobs/${blob.sha}`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }) if (blobResponse.ok) { const blobData = await blobResponse.json() let content = '' if (blobData.encoding === 'base64') { try { content = Buffer.from(blobData.content, 'base64').toString('utf-8') } catch { // Skip binary files return } } else { content = blobData.content } committedFiles[blob.path] = content } } catch (error) { console.error(`❌ GitHub Diff API: Error fetching blob ${blob.path}:`, error) } })) } // Compare current files with committed files const changes: Array<{ path: string changeType: 'added' | 'modified' | 'deleted' linesAdded: number linesRemoved: number currentContent: string committedContent: string }> = [] // Check for added and modified files currentFiles.forEach(currentFile => { const committedContent = committedFiles[currentFile.path] || '' if (currentFile.content !== committedContent) { const currentLines = currentFile.content.split('\n') const committedLines = committedContent.split('\n') changes.push({ path: currentFile.path, changeType: committedContent ? 'modified' : 'added', linesAdded: Math.max(0, currentLines.length - committedLines.length), linesRemoved: Math.max(0, committedLines.length - currentLines.length), currentContent: currentFile.content, committedContent }) } }) // Check for deleted files Object.keys(committedFiles).forEach(committedPath => { const currentFileExists = currentFiles.some(f => f.path === committedPath) if (!currentFileExists) { const committedContent = committedFiles[committedPath] const committedLines = committedContent.split('\n') changes.push({ path: committedPath, changeType: 'deleted', linesAdded: 0, linesRemoved: committedLines.length, currentContent: '', committedContent }) } }) const summary = { filesChanged: changes.length, insertions: changes.reduce((sum, change) => sum + change.linesAdded, 0), deletions: changes.reduce((sum, change) => sum + change.linesRemoved, 0) } return { changes, summary, diffText: generateDiffText(changes) } } // Helper function to generate unified diff text function generateDiffText(changes: Array<{ path: string changeType: 'added' | 'modified' | 'deleted' currentContent: string committedContent: string }>): string { let diffText = '' changes.forEach(change => { diffText += `\n--- a/${change.path}\n+++ b/${change.path}\n` if (change.changeType === 'added') { const lines = change.currentContent.split('\n') diffText += `@@ -0,0 +1,${lines.length} @@\n` lines.forEach(line => { diffText += `+${line}\n` }) } else if (change.changeType === 'deleted') { const lines = change.committedContent.split('\n') diffText += `@@ -1,${lines.length} +0,0 @@\n` lines.forEach(line => { diffText += `-${line}\n` }) } else { // Modified file - generate a simple unified diff const oldLines = change.committedContent.split('\n') const newLines = change.currentContent.split('\n') diffText += `@@ -1,${oldLines.length} +1,${newLines.length} @@\n` // Simple line-by-line comparison (not optimal but works) const maxLines = Math.max(oldLines.length, newLines.length) for (let i = 0; i < maxLines; i++) { const oldLine = oldLines[i] || '' const newLine = newLines[i] || '' if (oldLine !== newLine) { if (oldLines[i] !== undefined) diffText += `-${oldLine}\n` if (newLines[i] !== undefined) diffText += `+${newLine}\n` } else { diffText += ` ${oldLine}\n` } } } }) return diffText }